/**
 * Copyright © 2011 Creativor Studio.
 * About license information, please see LICENSE.txt.
 */
package org.creativor.rayson.client;

import java.io.IOException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.WeakHashMap;
import org.creativor.rayson.annotation.ClientVersion;
import org.creativor.rayson.api.AsyncRpcProtocol;
import org.creativor.rayson.api.RpcProtocol;
import org.creativor.rayson.api.TransferArgument;
import org.creativor.rayson.api.TransferSocket;
import org.creativor.rayson.common.Invocation;
import org.creativor.rayson.common.RpcCallException;
import org.creativor.rayson.exception.CallExecutionException;
import org.creativor.rayson.exception.IllegalMethodException;
import org.creativor.rayson.exception.IllegalServiceException;
import org.creativor.rayson.exception.NetWorkException;
import org.creativor.rayson.exception.RpcExceptionFactory;
import org.creativor.rayson.exception.ServiceNotFoundException;
import org.creativor.rayson.exception.UnsupportedVersionException;
import org.creativor.rayson.protocol.ServerProtocol;
import org.creativor.rayson.server.ServerService;
import org.creativor.rayson.transport.client.TransportClient;
import org.creativor.rayson.util.ProxyVerifier;

// TODO: Auto-generated Javadoc
/**
 * It acts as an client entrance of Rayson. All the client functions of Rayson,
 * the first entry point will be located here.
 * 
 * @author Nick Zhang
 */
class Client
{
	/**
	 * @author Nick Zhang
	 */
	private class AsyncProxyInvoker extends RpcProxyInvoker
	{
		/**
		 * 
		 * @param version
		 * @param serviceName
		 * @param serverAddress
		 */
		public AsyncProxyInvoker(short version, String serviceName, InetSocketAddress serverAddress)
		{
			super(version, serviceName, serverAddress);
		}
		
		@Override
		public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
		{
			// If it's getSession method, then invoke locally.
			if (method.getName().equals("getSession") && args == null) return getSession();
			
			if (getSession().isUnsupportedVersion())
			{
				CallFutureImpl callFuture = new CallFutureImpl(method.getExceptionTypes());
				callFuture.setException(new RpcCallException(getSession()
						.getUnsupportedVersionException()));
				return callFuture;
			}
			
			// TODO: add version info here.
			Invocation invocation = new Invocation(-1, method, args);
			
			ClientCall call =
					new ClientCall(getSession(), invocation, new CallFutureImpl(
							method.getExceptionTypes()));
			
			try
			{
				submitCall(getServerAddress(), call);
			} catch (IOException e)
			{
				throw new NetWorkException(e);
			}
			
			return call.getFuture();
		}
	}
	
	private class RpcProxyInvoker implements InvocationHandler
	{
		private ProxySession session;
		private InetSocketAddress serverAddress;
		
		public RpcProxyInvoker(short version, String serviceName, InetSocketAddress serverAddress)
		{
			this.serverAddress = serverAddress;
			this.session =
					new ProxySession(VERSION, version, serviceName, serverAddress.getAddress());
		}
		
		/**
		 * @return the serverAddress
		 */
		protected InetSocketAddress getServerAddress()
		{
			return serverAddress;
		}
		
		@Override
		public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
		{
			// If it's getSession method, then invoke locally.
			if (method.getName().equals("getSession") && args == null) return getSession();
			if (getSession().isUnsupportedVersion())
			{
				CallFutureImpl callFuture = new CallFutureImpl(method.getExceptionTypes());
				callFuture.setException(new RpcCallException(getSession()
						.getUnsupportedVersionException()));
				return callFuture.get();
			}
			// TODO: add version info here.
			Invocation invocation = new Invocation(-1, method, args);
			
			ClientCall call =
					new ClientCall(session, invocation, new CallFutureImpl(
							method.getExceptionTypes()));
			
			try
			{
				submitCall(getServerAddress(), call);
			} catch (IOException e)
			{
				throw RpcExceptionFactory.createNetWorkException(e);
			}
			
			try
			{
				return call.getResult();
			} catch (CallExecutionException e)
			{
				throw e.getCause();
			} catch (InterruptedException e)
			{
				throw RpcExceptionFactory.createUndecleardException(e);
			}
		}
		
		public ProxySession getSession()
		{
			return session;
		}
		
		@Override
		public String toString()
		{
			StringBuffer sb = new StringBuffer();
			sb.append("{");
			sb.append("session: ");
			sb.append(session.toString());
			sb.append("}");
			return sb.toString();
		}
	}
	
	private short defaultProxyVersion = ClientVersion.DEFAULT_VALUE;
	
	private static final byte VERSION = 1;
	
	/**
	 * Gets the version.
	 * 
	 * @return the version
	 */
	public static byte getVersion()
	{
		return VERSION;
	}
	
	private ResponseWorker responseWorker;
	
	private WeakHashMap<SocketAddress, ServerProtocol> serverServices;
	
	/**
	 * Instantiates a new client.
	 */
	Client()
	{
		ClientVersion proxyVersion = ServerProtocol.class.getAnnotation(ClientVersion.class);
		if (proxyVersion != null) defaultProxyVersion = proxyVersion.value();
		serverServices = new WeakHashMap<SocketAddress, ServerProtocol>();
		responseWorker = new ResponseWorker();
	}
	
	/**
	 * Creates the async proxy.
	 * 
	 * @param <T>
	 *            the generic type
	 * @param serviceName
	 *            the service name
	 * @param proxyInterface
	 *            the proxy interface
	 * @param serverAddress
	 *            the server address
	 * @return the t
	 * @throws IllegalServiceException
	 *             the illegal service exception
	 */
	public <T extends AsyncRpcProtocol> T createAsyncProxy(String serviceName,
			Class<T> proxyInterface, InetSocketAddress serverAddress)
			throws IllegalServiceException
	{
		if (serviceName == null || serviceName.isEmpty())
			throw new IllegalArgumentException("Service name should not be empty");
		if (proxyInterface == null)
			throw new IllegalArgumentException("Proxy interface  should not be null");
		if (serverAddress == null)
			throw new IllegalArgumentException("Server address name should not be null");
		
		ProxyVerifier.verifyAsyncProxy(proxyInterface);
		
		T rpcProxy;
		rpcProxy =
				(T) Proxy.newProxyInstance(Client.class.getClassLoader(),
						new Class[] { proxyInterface }, new AsyncProxyInvoker(
								getProxyVersion(proxyInterface), serviceName, serverAddress));
		return rpcProxy;
	}
	
	/**
	 * Creates the rpc proxy.
	 * 
	 * @param <T>
	 *            the generic type
	 * @param serviceName
	 *            the service name
	 * @param proxyInterface
	 *            the proxy interface
	 * @param serverAddress
	 *            the server address
	 * @return the t
	 * @throws IllegalServiceException
	 *             the illegal service exception
	 */
	public <T extends RpcProtocol> T createRpcProxy(String serviceName, Class<T> proxyInterface,
			InetSocketAddress serverAddress) throws IllegalServiceException
	{
		if (serviceName == null || serviceName.isEmpty())
			throw new IllegalArgumentException("Service name should not be empty");
		if (proxyInterface == null)
			throw new IllegalArgumentException("Proxy interface  should not be null");
		if (serverAddress == null)
			throw new IllegalArgumentException("Server address name should not be null");
		
		// verify rpc proxy interface.
		if (!proxyInterface.isInterface())
			throw new IllegalServiceException("Proxy class " + proxyInterface.getName()
					+ " must be an interface");
		
		// all interface must be rpc protocol.
		for (Class interfake : proxyInterface.getInterfaces())
		{
			if (!RpcProtocol.class.isAssignableFrom(interfake))
				throw new IllegalServiceException("Interface " + interfake.getName()
						+ " must be an " + RpcProtocol.class.getName());
		}
		// verify every method.
		for (Method proxyMethod : proxyInterface.getMethods())
		{
			// Ignore getSession method.
			if (proxyMethod.getName().equals("getSession")
					&& proxyMethod.getParameterTypes().length == 0) continue;
			try
			{
				ProxyVerifier.verifyProxyMethod(proxyMethod);
			} catch (IllegalMethodException e)
			{
				throw new IllegalServiceException(e.getMessage());
			}
		}
		
		T rpcProxy;
		rpcProxy =
				(T) Proxy.newProxyInstance(Client.class.getClassLoader(),
						new Class[] { proxyInterface }, new RpcProxyInvoker(
								getProxyVersion(proxyInterface), serviceName, serverAddress));
		return rpcProxy;
	}
	
	private short getProxyVersion(Class<? extends RpcProtocol> proxyInterface)
	{
		short proxyVersion = ClientVersion.DEFAULT_VALUE;
		ClientVersion annotation = proxyInterface.getAnnotation(ClientVersion.class);
		if (annotation != null) proxyVersion = annotation.value();
		return proxyVersion;
	}
	
	/**
	 * Get a PRC server proxy of one remote server.
	 * 
	 * @param <T>
	 *            RPC proxy type.
	 * @param serverAddress
	 *            Server socket address.
	 * @return An RPC proxy object which can communicate with server in RPC way.
	 */
	<T extends RpcProtocol> T getServerProxy(InetSocketAddress serverAddress)
	{
		ServerProtocol rpcService;
		synchronized (serverServices)
		{
			rpcService = serverServices.get(serverAddress);
			if (rpcService == null)
			{
				rpcService =
						(ServerProtocol) Proxy.newProxyInstance(Client.class.getClassLoader(),
								new Class[] { ServerProtocol.class }, new RpcProxyInvoker(
										defaultProxyVersion, ServerService.NAME, serverAddress));
				serverServices.put(serverAddress, rpcService);
			}
			
		}
		return (T) rpcService;
	}
	
	/**
	 * Start this client. Make it can start to handling rpc request and
	 * response. It also start the {@link TransportClient}.
	 * 
	 * @throws RuntimeException
	 *             If start the {@link TransportClient} failed.
	 */
	void start() throws RuntimeException
	{
		responseWorker.start();
		TransportClient.getSingleton().start();
	}
	
	/**
	 * Open transfer socket.
	 * 
	 * @param serverAddress
	 *            the server address
	 * @param argument
	 *            the argument
	 * @return the transfer socket
	 * @throws IOException
	 *             Signals that an I/O exception has occurred.
	 * @throws ServiceNotFoundException
	 *             the service not found exception
	 * @throws IllegalServiceException
	 *             the illegal service exception
	 * @throws UnsupportedVersionException
	 *             the unsupported version exception
	 */
	public TransferSocket
			openTransferSocket(SocketAddress serverAddress, TransferArgument argument)
					throws IOException, ServiceNotFoundException, IllegalServiceException,
					UnsupportedVersionException
	{
		if (argument == null)
			throw new IllegalArgumentException("Transfer argument  should not be null");
		if (serverAddress == null)
			throw new IllegalArgumentException("Server address name should not be null");
		return TransportClient.getSingleton().getConnector()
				.openTransferSocket(serverAddress, argument);
	}
	
	/**
	 * Ping.
	 * 
	 * @param serverAddress
	 *            the server address
	 * @throws NetWorkException
	 *             the net work exception
	 */
	public void ping(SocketAddress serverAddress) throws NetWorkException
	{
		TransportClient.getSingleton().getConnector().ping(serverAddress);
	}
	
	private void submitCall(SocketAddress serverAddress, ClientCall call) throws IOException,
			InterruptedException
	{
		TransportClient.getSingleton().getConnector().submitCall(serverAddress, call);
	}
	
}
